using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Globalization;

using BizElements.Core;

namespace BizElements.BusinessLayer
{
    /// <summary>
    /// Manages settings in the 'AppSettings' table.
    /// </summary>
    /// <remarks>The implementation automatically remotes all data-access operations in accordance 
    /// with the configuration provided by <see cref="BizElements.BusinessLayer.RpcHelper"/>.</remarks>
    [Serializable]
    public sealed class AppSettingsManager : IAppSettingsManager
    {
        #region Constants.

        const int deactivated = 0;
        const int active = 1;

        #endregion

        #region Find.

        /// <summary>
        /// Retrieves all settings that match the filter criteria.
        /// </summary>
        /// <param name="filter">Partially populated <see cref="IAppSetting"/> object whose properties define a filter criteria.</param>
        /// <returns>A list of settings which match the filter criteria.</returns>
        public IList<IAppSetting> Find(IAppSetting filter)
        {            
            string cacheKey = GetCacheKey(filter, AppSettingsFilterOptions.None);

            IList<IAppSetting> globalSettings = GlobalCache.GetData(cacheKey) as IList<IAppSetting>;
            if (globalSettings == null)
            {
                EntityCollection<AppSettingsEntity, AppSettingsMeta> dbEntities = (EntityCollection<AppSettingsEntity, AppSettingsMeta>)RpcHelper.InvokeStaticMethod(GetType(), "DbFindEntities", filter, Time.Now, true, SettingsScope.GlobalSettings, ModuleContextCondition.MatchExactContext);
                globalSettings = DbEntitiesToAppSettings(dbEntities);
                GlobalCache.SetData(cacheKey, globalSettings, GetGlobalCacheDependencies(filter, AppSettingsFilterOptions.None));
            }

            IList<IAppSetting> userSettings = null;
            IActor user = (filter.ActorId != null) ? new Actor(filter.ActorId) : null;
            if (user != null)
            {
                userSettings = UserCache.GetData(user, cacheKey) as IList<IAppSetting>;
                if (userSettings == null)
                {
                    EntityCollection<AppSettingsEntity, AppSettingsMeta> dbEntities = (EntityCollection<AppSettingsEntity, AppSettingsMeta>)RpcHelper.InvokeStaticMethod(GetType(), "DbFindEntities", filter, Time.Now, true, SettingsScope.UserSettings, ModuleContextCondition.MatchExactContext);
                    userSettings = DbEntitiesToAppSettings(dbEntities);
                    UserCache.SetData(user, cacheKey, userSettings, GetUserCacheDependencies(filter, AppSettingsFilterOptions.None));
                }
            }

            IList<IAppSetting> allSettings = MergeSettings(globalSettings, userSettings);
            return allSettings;
        }

        /// <summary>
        /// Retrieves all settings that match the filter criteria.
        /// </summary>
        /// <param name="filter">Partially populated <see cref="IAppSetting"/> object whose properties define a filter criteria.</param>
        /// <param name="options">Specifies flags that control how filter is interpreted.</param>
        /// <returns>A list of settings which match the filter criteria.</returns>
        public IList<IAppSetting> Find(IAppSetting filter, AppSettingsFilterOptions options)
        {
            if (options == AppSettingsFilterOptions.None)
                return Find(filter);

            string cacheKey = GetCacheKey(filter, options);
            ModuleContextCondition contextCondition = DetermineContextCondition(filter, options);

            IList<IAppSetting> globalSettings = null;
            bool fetchGlobalSettings = (filter.ActorId == null) || (options & AppSettingsFilterOptions.IncludeGlobalSettings) > 0;
            if (fetchGlobalSettings)
            {
                globalSettings = GlobalCache.GetData(cacheKey) as IList<IAppSetting>;
                if (globalSettings == null)
                {
                    EntityCollection<AppSettingsEntity, AppSettingsMeta> dbEntities = (EntityCollection<AppSettingsEntity, AppSettingsMeta>)RpcHelper.InvokeStaticMethod(GetType(), "DbFindEntities", filter, Time.Now, true, SettingsScope.GlobalSettings, contextCondition);
                    globalSettings = DbEntitiesToAppSettings(dbEntities);
                    GlobalCache.SetData(cacheKey, globalSettings, GetGlobalCacheDependencies(filter, options));
                }
            }

            IList<IAppSetting> userSettings = null;
            IActor user = (filter.ActorId != null) ? new Actor(filter.ActorId) : null;
            if (user != null)
            {
                userSettings = UserCache.GetData(user, cacheKey) as IList<IAppSetting>;
                if (userSettings == null)
                {
                    EntityCollection<AppSettingsEntity, AppSettingsMeta> dbEntities = (EntityCollection<AppSettingsEntity, AppSettingsMeta>)RpcHelper.InvokeStaticMethod(GetType(), "DbFindEntities", filter, Time.Now, true, SettingsScope.UserSettings, contextCondition);
                    userSettings = DbEntitiesToAppSettings(dbEntities);
                    UserCache.SetData(user, cacheKey, userSettings, GetUserCacheDependencies(filter, options));
                }
            }

            IList<IAppSetting> allSettings = MergeSettings(globalSettings, userSettings);
            return allSettings;
        }

        private static ModuleContextCondition DetermineContextCondition(IAppSetting filter, AppSettingsFilterOptions options)
        {
            bool includeContextual = (options & AppSettingsFilterOptions.IncludeContextualSettings) > 0;
            bool includeContextFree = (options & AppSettingsFilterOptions.IncludeContextFreeSettings) > 0;
            ModuleContextCondition contextCondition;
            if (string.IsNullOrEmpty(filter.Context) && includeContextual)
                contextCondition = ModuleContextCondition.AllContexts;
            else if (!string.IsNullOrEmpty(filter.Context) && includeContextFree)
                contextCondition = ModuleContextCondition.AllContexts;
            else
                contextCondition = ModuleContextCondition.MatchExactContext;

            return contextCondition;
        }

        private static EntityCollection<AppSettingsEntity, AppSettingsMeta> DbFindEntities(IAppSetting filter, DateTime? date, bool onlyActive, SettingsScope scope, ModuleContextCondition condition)
        {
            IEntityDAO dao = GetDAO(new AppSettingsEntity());
            SearchCondition recordsToFetch = CreateFilter(filter, date, onlyActive, scope, condition);
            EntityCollection<AppSettingsEntity, AppSettingsMeta> dbEntities = (EntityCollection<AppSettingsEntity, AppSettingsMeta>)dao.Select(null, recordsToFetch);
            return dbEntities;
        }

        #endregion

        #region Delete.

        /// <summary>
        /// Deletes all settings that match the filter criteria.
        /// </summary>
        /// <param name="filter">Partially populated <see cref="IAppSetting"/> object whose properties define a filter criteria.</param>
        public void Delete(IAppSetting filter)
        {
            RpcHelper.InvokeStaticMethod<IAppSetting>(DbDelete, filter);

            // Remove all dependent cache entries.
            string[] cacheCatsToRemove = (filter.ActorId != null) ? GetUserCacheDependencies(filter, AppSettingsFilterOptions.None) : GetGlobalCacheDependencies(filter, AppSettingsFilterOptions.None);
            GlobalCache.RemoveCategories(cacheCatsToRemove);
        }        

        private static void DbDelete(IAppSetting filter)
        {
            AppSettingsMeta table = new AppSettingsMeta();
            SettingsScope globalOrUser = (filter.ActorId == null) ? SettingsScope.GlobalSettings : SettingsScope.UserSettings;
            SearchCondition recordsToDeactivate = CreateFilter(filter, null, true, globalOrUser, ModuleContextCondition.MatchExactContext);
            IEntityDAO dao = GetDAO(new AppSettingsEntity()); 
            dao.Update(new UpdateList(table.Status, deactivated), recordsToDeactivate);
        }

        #endregion

        #region Save.

        /// <summary>
        /// Saves the provided setting.
        /// </summary>
        /// <param name="setting">Settings information to save.</param>
        public void Save(IAppSetting setting)
        {
            RpcHelper.InvokeStaticMethod<IAppSetting>(DbSave, setting);

            // Remove all dependent cache entries.
            string[] cacheCatsToRemove = (setting.ActorId != null) ? GetUserCacheDependencies(setting, AppSettingsFilterOptions.None) : GetGlobalCacheDependencies(setting, AppSettingsFilterOptions.None);
            GlobalCache.RemoveCategories(cacheCatsToRemove);
        }

        private static void DbSave(IAppSetting setting)
        {
            SettingsScope globalOrUser = (setting.ActorId == null) ? SettingsScope.GlobalSettings : SettingsScope.UserSettings;
            EntityCollection<AppSettingsEntity, AppSettingsMeta> existingDbRecords = DbFindEntities(setting, null, true, globalOrUser, ModuleContextCondition.MatchExactContext);
            if (existingDbRecords.Count == 1)
            {
                // Update existing.
                (existingDbRecords[0] as IAppSetting).Value = setting.Value;
                GetDAO(existingDbRecords[0]).UpdateOne();
            }
            else
            { 
                // Insert new.
                AppSettingsEntity entity = SettingToEntity(setting);
                entity.Status = active;
                entity.ValidFrom = Time.Now.Date;
                GetDAO(entity).InsertOne();
            }
        }

        #endregion

        #region NewSetting.

        /// <summary>
        /// Creates a new empty <see cref="IAppSetting"/> object compatibile with the current manager.
        /// </summary>
        /// <returns><see cref="IAppSetting"/> object.</returns>
        public IAppSetting NewSetting()
        {
            return new AppSettingsEntity();
        }

        #endregion

        private static IEntityDAO GetDAO(IEntity entity)
        {
            return EntityDaoFactory.GetEntityDAO(entity, null);
        } 

        #region Cache.

        const string cacheKeyPrefix = "AppSettingsManager.e563dec0b9a54546a6d642f4c532cce5";
        const string globalCacheKategory = cacheKeyPrefix + "_Global";
        const string userCacheKategoryPrefix = cacheKeyPrefix + "_User_";

        private static string GetCacheKey(IAppSetting setting, AppSettingsFilterOptions options)
        {
            string key = cacheKeyPrefix + "_Global";
            AppendIfNotNull(ref key, setting.Application);
            AppendIfNotNull(ref key, setting.Module);
            AppendIfNotNull(ref key, setting.Context);
            AppendIfNotNull(ref key, setting.Component);
            AppendIfNotNull(ref key, setting.PropertyPathString);
            if (options != AppSettingsFilterOptions.None)
                key += "_" + Convert.ToInt32(options);

            return key;
        }

        private static string[] GetGlobalCacheDependencies(IAppSetting setting, AppSettingsFilterOptions options)
        {
            return new string[] { globalCacheKategory };
        }

        private static string[] GetUserCacheDependencies(IAppSetting setting, AppSettingsFilterOptions options)
        {
            return new string[] { userCacheKategoryPrefix + setting.ActorId.ToString() };
        }

        private static void AppendIfNotNull(ref string receiver, object appendix)
        {
            if (appendix != null)
                AppendIfNotNull(ref receiver, appendix.ToString());
        }

        private static void AppendIfNotNull(ref string receiver, string appendix)
        {
            if (!string.IsNullOrEmpty(appendix))
                receiver += "_" + appendix;
        }

        #endregion

        #region Conversion, collections.

        private static AppSettingsEntity SettingToEntity(IAppSetting setting)
        {
            IAppSetting entity = new AppSettingsEntity();
            entity.ActorId = setting.ActorId;
            entity.Application = setting.Application;
            entity.Component = setting.Component;
            entity.Context = setting.Context;
            entity.Module = setting.Module;
            entity.PropertyPathString = setting.PropertyPathString;
            entity.Value = setting.Value;

            return (AppSettingsEntity)entity;
        }

        private static IList<IAppSetting> DbEntitiesToAppSettings(EntityCollection<AppSettingsEntity, AppSettingsMeta> dbEntities)
        {
            List<IAppSetting> appSettings = new List<IAppSetting>(dbEntities.Count);
            foreach (AppSettingsEntity entity in dbEntities)
                appSettings.Add(entity);

            return appSettings;
        }

        private static IList<IAppSetting> MergeSettings(params IList<IAppSetting>[] globalAndUserSettings)
        {
            List<IAppSetting> allSettings = new List<IAppSetting>();
            for (int idxArray = 0; idxArray < globalAndUserSettings.Length; idxArray++)
            {
                if (globalAndUserSettings[idxArray] != null)
                {
                    foreach (IAppSetting setting in globalAndUserSettings[idxArray])
                        allSettings.Add(setting);
                }
            }

            return allSettings;
        }

        #endregion

        #region CreateFilter.

        enum SettingsScope
        { 
            AllSettings,
            UserSettings,
            GlobalSettings
        }

        enum ModuleContextCondition
        { 
            AllContexts,
            MatchExactContext
        }

        private static SearchCondition CreateFilter(IAppSetting setting, DateTime? date, bool onlyActive, SettingsScope scope, ModuleContextCondition condition)
        {                        
            AppSettingsMeta table = new AppSettingsMeta();
            SearchCondition filter = new SearchCondition();
            
            if (onlyActive)
                filter.And(PredicateFactory.Compare(table.Status, "=", active));

            // Determine whether to fetch settings which are only valid for the given date.
            bool fetchOnlyValidSettings = (date != null);
            if (fetchOnlyValidSettings)
            {
                filter.And(PredicateFactory.Compare(table.ValidFrom, "<=", date.Value));
                SearchCondition notExpired = new SearchCondition(PredicateFactory.IsNull(table.ValidUntil));
                notExpired.Or(PredicateFactory.Compare(table.ValidUntil, ">=", date.Value));
                filter.And(notExpired);
            }

            // Determine scope: user/global/combined.
            bool isUserDefined = (setting.ActorId != null) && !string.IsNullOrEmpty(setting.ActorId.ToString());
            bool fetchGlobalSettings = (scope == SettingsScope.AllSettings) || (scope == SettingsScope.GlobalSettings);
            bool fetchUserSetttings = isUserDefined && (scope == SettingsScope.AllSettings) || (scope == SettingsScope.UserSettings);
            bool fetchAllSettings = (fetchUserSetttings && fetchGlobalSettings);
            if (fetchAllSettings)
            {
                SearchCondition globalOrUserSettings = new SearchCondition(PredicateFactory.IsNull(table.ActorId));
                globalOrUserSettings.Or(PredicateFactory.Compare(table.ActorId, "=", setting.ActorId));
                filter.And(globalOrUserSettings);
            }
            else
            {
                if (fetchUserSetttings)
                    filter.And(PredicateFactory.Compare(table.ActorId, "=", setting.ActorId));
                else if (fetchGlobalSettings)
                    filter.And(PredicateFactory.IsNull(table.ActorId));
                else
                    throw new InvalidOperationException("Cannot determine the scope of records to fetch.");
            }

            if (!string.IsNullOrEmpty(setting.Application))
                filter.And(PredicateFactory.Compare(table.Application, "=", setting.Application));

            if (!string.IsNullOrEmpty(setting.Module))
                filter.And(PredicateFactory.Compare(table.Module, "=", setting.Module));

            // Typically MatchExactContext is used. Only admin modules should fetch all contexts at once.
            if (condition == ModuleContextCondition.MatchExactContext)
            {
                if (!string.IsNullOrEmpty(setting.Context))
                    filter.And(PredicateFactory.Compare(table.Context, "=", setting.Context));
                else
                    filter.And(PredicateFactory.IsNull(table.Context));
            }

            if (!string.IsNullOrEmpty(setting.Component))
                filter.And(PredicateFactory.Compare(table.Component, "=", setting.Component));

            if (!string.IsNullOrEmpty(setting.PropertyPathString))
                filter.And(PredicateFactory.Compare(table.PropertyPathString, "=", setting.PropertyPathString));
            
            return filter;
        }

        #endregion
    }
}
